﻿#include "operationhandler.h"
#include <QSemaphore>
#include <QStandardItem>
#include <QTextEncoder>
#include <QTextDecoder>
#include <QThreadPool>
#include <QTextStream>
#include <QDir>
#include <QTime>
#include <QXmlStreamWriter>
#include <QPrinter>
#include <QPainter>
#include <QTextDocument>
#include "operationhelper.h"

#define APP_WIN_HEADER 					"ContentSeacher"
#define TEMP_FILE_SUFFIX				"%1.tmp"
#define BLUE_FONT 						"<font color=blue>%1</font>"
#define CLOCKS_PER_SEC					1000
#define NUMBER_OF_PARALLEL_SEARCHER		10
namespace
{
	int nMaxBlockSize = 4096;
	int nBlockSize = 128;
}

OperationHandler::OperationHandler(QObject *object, MessageDeliver *msgDeliver)
	: QThread(object), m_pMessageDeliver(msgDeliver)
{
	m_isStop = false;

	m_pThreadPool = new QThreadPool(this);
	m_pThreadPool->setMaxThreadCount(NUMBER_OF_PARALLEL_SEARCHER);

	m_nTotalNeedSearchFile = 0;
	m_pSemaphore = new QSemaphore(1);
	m_pSemaphoreParallel = new QSemaphore(NUMBER_OF_PARALLEL_SEARCHER);

	connect(m_pMessageDeliver, SIGNAL(txReplyValue(int)), this, SLOT(setReply(int)));
	connect(m_pMessageDeliver, SIGNAL(txParallelWorkCompleted()), this, SLOT(parallelWorkCompleted()), Qt::QueuedConnection);
}

OperationHandler::~OperationHandler()
{
	delete m_pSemaphore;
	delete m_pSemaphoreParallel;
}

void OperationHandler::parallelWorkCompleted()
{
	m_pSemaphoreParallel->release();
}

void OperationHandler::setReply(int reply)
{
	m_replyLock.lockForWrite();
	m_nReplyValue = reply;
	m_replyLock.unlock();
}

void OperationHandler::setReplaceAll()
{
	m_isReplaceAll = true;
}

void OperationHandler::setReplaceDir()
{
	m_isReplaceDir = true;
}

void OperationHandler::setReplaceFile()
{
	m_isReplaceFile = true;
}

void OperationHandler::setSkipAll()
{
	m_isSkipAll = true;
}

void OperationHandler::setSkipDir()
{
	m_isSkipDir = true;
}

void OperationHandler::setSkipFile()
{
	m_isSkipFile = true;
}


bool OperationHandler::setSearchParameter(const QDir &searchDir, const QString &dirFilter, const QString &pattern, bool isCaseSensitive, const QString &encoding /* ANSI */)
{
	if (!this->isRunning())
	{
		m_searchDir = searchDir;
		m_strFileFilter = dirFilter;
		m_isCase = isCaseSensitive;
		m_option = OperationHandler::DoSearch;
		m_strSearchPattern = pattern;
		return true;
	}
	return false;
	
}

bool OperationHandler::setReplaceParameter(const QDir &searchDir, const QString &dirFilter, const QString &pattern, bool isCaseSensitive, const QString &replaceContent, const QString &encoding /* ANSI */)
{
	if (!this->isRunning())
	{
		m_searchDir = searchDir;
		m_strFileFilter = dirFilter;
		m_isCase = isCaseSensitive;
		m_strReplacement = replaceContent;
		m_option = OperationHandler::DoSearchAndReplace;
		m_strSearchPattern = pattern;
		return true;
	}
	return false;
	
}

bool OperationHandler::setExportXMLParameter(const QString &path, const QStandardItemModel *model)
{
	if (!this->isRunning())
	{
		m_strOutPutPath = path;
		m_pResultModel = model;
		m_option = OperationHandler::DoExportToXML;
		return true;
	}
	return false;
}

bool OperationHandler::setPrintResultParameter(const QStandardItemModel *model)
{
	if (!this->isRunning())
	{
		m_pResultModel = model;
		m_option = OperationHandler::DoPrintResult;
		return true;
	}
	return false;
}

void OperationHandler::stop()
{
	if(this->isRunning())
	{
		m_isStop = true;
		m_pSemaphore->acquire();
		m_pSemaphore->release();
	}
}

void OperationHandler::doReplace()
{
	QFileInfoList list = retrievalDIr(m_searchDir, m_strFileFilter, false);  // Filter files for searching.
	// If no files after filtering.
	if(list.size() == 0)
	{ 
		m_pMessageDeliver->popUpWarning(tr(APP_WIN_HEADER), tr("No match files under the path"));
		m_pMessageDeliver->searchReplaceCompleted(NULL);
		return;
	}

	m_pMessageDeliver->initialProgressRange(list.size());
	
	int totalMatch = 0;
	int totalFile = 0;
	int totalRep = 0;

	m_isReplaceFile = false;
	m_isReplaceDir = false;
	m_isReplaceAll = false;
	m_isSkipFile = false;
	m_isSkipDir = false;
	m_isSkipAll = false;
	m_isYesToAll = false;
	m_isNoToAll = false;
		
	QStandardItemModel *replaceListModel = new QStandardItemModel;

	for(int i = 0; i < list.size(); i++)
	{
		QFileInfo fileInfo = list.at(i);
		if(fileInfo.isFile()){

			QStandardItem *searchResultItem = new QStandardItem;
			searchResultItem->setAccessibleText(fileInfo.absoluteFilePath().toLocal8Bit());
			QString resultLabel = tr("Filter %1 files").arg(i);
			m_pMessageDeliver->updateResultLabel(resultLabel);
			QString originalFilePath = fileInfo.absoluteFilePath();
			QString tempFilePath = tr(TEMP_FILE_SUFFIX).arg(fileInfo.absoluteFilePath());
			
			int replaceCount = processSearchReplaceToTemp(originalFilePath, tempFilePath, searchResultItem);

			searchResultItem->setText(tr("<font color=blue>Match %1  Replace %2 in</font> %3").arg(searchResultItem->rowCount()).arg(replaceCount).arg(fileInfo.absoluteFilePath()));
			if(searchResultItem->hasChildren())
			{
				totalMatch += searchResultItem->rowCount();
				totalFile++;
				m_pMessageDeliver->updateCompletedItem(searchResultItem);
			}

			if (replaceCount)
			{
				confirmReplaceToOriginal(originalFilePath, tempFilePath, searchResultItem);
			}
			else
			{
				QFile::remove(tempFilePath);
			}
			
			m_isReplaceFile = false;
			m_isSkipFile = false;

		} 
		else if(fileInfo.isDir())
		{
			m_isReplaceDir = false;
			m_isSkipDir = false;
		}


		if(m_isSkipAll || m_isStop)
		{
			QString ret = tr("Interrupt: Total search %1   match %2   replace %3  in %4  files").arg(i).arg(totalMatch).arg(totalRep).arg(totalFile);
			m_pMessageDeliver->searchReplaceCompleted(ret);
			return;
		}

	}
	QString ret = tr("Result: Total search %1  match %2  replace %3  in %4  files").arg(list.size()).arg(totalMatch).arg(totalRep).arg(totalFile);
	m_pMessageDeliver->searchReplaceCompleted(ret);
	
}

void OperationHandler::doSearch()
{
	QFileInfoList list = retrievalDIr(m_searchDir, m_strFileFilter, false);  // Filter files for searching.
	// If no files after filtering.
	if(list.size() == 0)
	{ 
		m_pMessageDeliver->popUpWarning(tr(APP_WIN_HEADER), tr("No match files under the path"));
		m_pMessageDeliver->searchReplaceCompleted(NULL);
		return;
	}

	m_pMessageDeliver->initialProgressRange(list.size());

	QTime runTimeCnt;
	runTimeCnt.restart();

	m_nTotalNeedSearchFile = 0;	// Total searched files.
	//totalFile = 0;  // Total files number after filetering.
	
	for(int i = 0; i < list.size() && !m_isStop; i++)
	{
		QFileInfo fileInfo = list.at(i);
		if (!fileInfo.isFile())
		{
			continue;
		}

		m_pSemaphoreParallel->acquire();
		ParallelSearcher *parallelSearcher = new ParallelSearcher(m_pMessageDeliver);
		parallelSearcher->setSearchFile(fileInfo.absoluteFilePath(), m_strSearchPattern, m_isCase, m_isStop);
		m_pThreadPool->start(parallelSearcher);

	}

	m_pSemaphore->acquire();
	m_pThreadPool->waitForDone();
	m_pSemaphore->release();

	int runTime = runTimeCnt.elapsed();

	if(m_isStop)
	{
		QString ret = tr("Interrupt: Total files %1  search%3  match %4  in%5 files  time: %2s").arg(list.size()).arg(static_cast<double>(runTime)/CLOCKS_PER_SEC);
		m_pMessageDeliver->searchReplaceCompleted(ret);
		return;
	}

	QString ret = tr("Result:  Total files %1  search %3  match %4  in%5 files  time: %2s").arg(list.size()).arg(static_cast<double>(runTime)/CLOCKS_PER_SEC);
	m_pMessageDeliver->searchReplaceCompleted(ret);
}

void OperationHandler::doExportToXML()
{
	QFile file(m_strOutPutPath);
	QXmlStreamWriter xmlWriter;
	if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
	{
		m_pMessageDeliver->popUpWarning(tr(APP_WIN_HEADER), tr("Export XML failed"));
	}

	m_pMessageDeliver->initialProgressRange(m_pResultModel->rowCount());

	xmlWriter.setDevice(&file);
	xmlWriter.setAutoFormatting(true);
	xmlWriter.writeStartDocument();
	xmlWriter.writeStartElement("SearchResult");

	for (int i = 0; i < m_pResultModel->rowCount() && !m_isStop; i++)
	{
		m_pMessageDeliver->updateProgressBar(i, "");
		QStandardItem *item = m_pResultModel->item(i);
		xmlWriter.writeStartElement("File");
		xmlWriter.writeAttribute("FileName", item->accessibleText());
		if (item->hasChildren())
		{
			for (int j = 0; j < item->rowCount(); j++)
			{
				xmlWriter.writeStartElement("MatchContent");
				xmlWriter.writeCDATA(item->child(j)->accessibleText());
				xmlWriter.writeEndElement();
			}
		}
		xmlWriter.writeEndElement();
	}
	xmlWriter.writeEndElement();
	xmlWriter.writeEndDocument();
	m_pMessageDeliver->otherOperationCompleted();
	file.close();
}

void OperationHandler::doPrintResult()
{
	QPrinter printer;
	QPainter painter(&printer);
	painter.setFont(QFont("Arial"));

	QStringList stringList;

	int k = 0;

	m_pMessageDeliver->initialProgressRange(m_pResultModel->rowCount());

	for(int i = 0; i < m_pResultModel->rowCount() && !m_isStop; i++)
	{
		m_pMessageDeliver->updateProgressBar(i, "");
		QStandardItem *item = m_pResultModel->item(i);

		stringList.push_back(QString::fromLocal8Bit("<b>%1</b>").arg(item->accessibleText()));
		if(item->hasChildren())
		{
			for(int j = 0; j < item->rowCount(); j++)
			{
				stringList.push_back(item->child(j)->accessibleText());
			}
		}
	}

	QTextDocument doc;
	doc.setHtml(stringList.join("<br><br>"));
	doc.setPageSize(printer.pageRect().size());

	QRect innerRect = printer.pageRect();
	innerRect.setTop(innerRect.top());
	innerRect.setBottom(innerRect.bottom());
	QRect contentRect = QRect(QPoint(0,0), doc.size().toSize());
	QRect currentRect = QRect(QPoint(0,0), innerRect.size());

	int count = 0;
	while (currentRect.intersects(contentRect) && !m_isStop) 
	{
		doc.drawContents(&painter, currentRect);
		count++;
		currentRect.translate(0, currentRect.height());
		painter.translate(0, -currentRect.height());
		if (currentRect.intersects(contentRect))
			printer.newPage();
	}
	m_pMessageDeliver->otherOperationCompleted();
	painter.restore();
	painter.end();
}

void OperationHandler::run()
{
	if(m_pMessageDeliver == nullptr)
	{
		return;
	}
	m_isStop = false;
	
	if (m_option == OperationHandler::DoSearch)
	{
		doSearch();
	}
	if (m_option == OperationHandler::DoSearchAndReplace)
	{
		doReplace();
	}
	if (m_option == OperationHandler::DoExportToXML)
	{
		doExportToXML();
	}
	if (m_option == OperationHandler::DoPrintResult)
	{
		doPrintResult();
	}
}

QFileInfoList OperationHandler::retrievalDIr(const QDir &dir, const QString &pattern, const bool isIncludeDir)
{
	QFileInfoList list;	
	QFileInfoList retList;
	list = dir.entryInfoList();

	for(int i = 0; i < list.size() && !m_isStop; i++)
	{
		QFileInfo fileInfo = list.at(i);
		if(fileInfo.isDir())
		{
			QDir subDir;
			subDir.setFilter(dir.filter());
			subDir.setSorting(dir.sorting());
			subDir.setPath(fileInfo.absoluteFilePath());
			retList.append(retrievalDIr(subDir, pattern, isIncludeDir));
			if(isIncludeDir)
			{
				retList.push_back(fileInfo);
			}
		}else if(fileInfo.isFile())
		{
			if(QDir::match(pattern, fileInfo.fileName()))
			{
				retList.push_back(fileInfo);
				m_nTotalNeedSearchFile++;
			}
		}
	}
	QString resultLabel = tr("Filter %1 files").arg(m_nTotalNeedSearchFile);
	m_pMessageDeliver->updateResultLabel(resultLabel);
	return retList;
}

void OperationHandler::confirmReplaceToOriginal(const QString &originalFilePath, const QString &tempFilePath, QStandardItem *searchResultItem)
{
	
	if (!m_isYesToAll && !m_isNoToAll)
	{
		m_pMessageDeliver->popUpQuestion(tr(APP_WIN_HEADER), tr("Confirm to Replace?").append(originalFilePath));
	}
	else if (m_isNoToAll)
	{
		setReply(0);
	}
	else if (m_isYesToAll)
	{
		setReply(1);
	}


	m_replyLock.lockForRead();
	if (m_nReplyValue == 2)
	{
		m_isYesToAll = true;
	}
	else if (m_nReplyValue == 3)
	{
		m_isNoToAll = true;
	}

	if(m_nReplyValue)
	{
		if(commitReplace(originalFilePath, tempFilePath) == -1)
		{
			m_pMessageDeliver->popUpWarning(tr(APP_WIN_HEADER), tr("Replace Failed !").append(tempFilePath.toUtf8()));
			m_pMessageDeliver->updateFialedItem(tr("<font color=blue>-------- Replace Failed! File RollBack --------</font>"));
		}
	}
	else
	{
		QFile::remove(tempFilePath);
		m_pMessageDeliver->updateFialedItem(tr("<font color=blue>-------- File RollBack --------</font>") + originalFilePath);

	}
	m_replyLock.unlock();
	setReply(0);
}

/**
	Two things to be done in this function.
	1. Call messageDeliver to pop up a question dialog for asking use weather need to replace this 
	item(a place found in this line).
	2. If need to replace, then call regex_replace to replace and append the replacement to replaceLine. 
	otherwise, append to original line to replaceLine.

	@param[in]		originalContent: the content which has been found including pattern and need to be replace.
	@param[out]		replaceListModel: display on replace dialog, to show the previous process in current file.
	@param[out]		replaceLine: the replacement string write to temp file.
	@param[out]		contentLineDisp: the display content of item which will be append to result view in main dialog.
**/
bool OperationHandler::processItemReplace(
	QString &originalContent, 
	QStandardItemModel *replaceListModel, 
	QString &replaceLine, 
	QString &contentLineDisp)
{
	int reply = 1;
	if(!(m_isSkipAll || m_isSkipDir || m_isSkipFile ||
		m_isReplaceAll || m_isReplaceDir || m_isReplaceFile))
	{
		m_pMessageDeliver->popUpReplace(replaceListModel);
		m_replyLock.lockForRead();
		reply += m_nReplyValue;
		m_nReplyValue = 0;
		m_replyLock.unlock();
	}

	if(!(m_isSkipAll || m_isSkipDir || m_isSkipFile || !reply) 
		&& (m_isReplaceAll || m_isReplaceDir || m_isReplaceFile || reply))
	{
		QRegExp regExp(m_strSearchPattern);
		originalContent.replace(regExp, m_strReplacement);
		replaceLine.append(originalContent);
		QStandardItem *rep = new QStandardItem(tr("Replace..."));
		replaceListModel->appendRow(rep);
		contentLineDisp.append(tr(BLUE_FONT).arg(tr(" - Replaced")));
		return true;
	}
	else
	{
		QStandardItem *skip = new QStandardItem(tr("Skip ..."));
		replaceListModel->appendRow(skip);
		replaceLine.append(originalContent);
		return false;
	}
}

/**
	Search and replace in a single file. Search line by line, if found a match content in searching line,
	pop up a dialog ask use if need to replace. Replace result will write to a temp file waiting to afterward
	processing.

	@param[in]		originalFilePath: the original file path.
	@param[in]		tempFilePath: the temp file path.
	@param[out]		searchResultItem: the result item will be display in result tree view.
**/
int OperationHandler::processSearchReplaceToTemp(const QString &originalFilePath, const QString &tempFilePath, QStandardItem *searchResultItem)
{
	QFile originalFile(originalFilePath);
	QFile tempFile(tempFilePath);
	if (!originalFile.open(QFile::ReadOnly) || !tempFile.open(QFile::WriteOnly | QFile::Truncate))
	{
		originalFile.close();
		tempFile.close();
		m_pMessageDeliver->popUpWarning(tr(APP_WIN_HEADER), tr("Open Failed ").append(originalFilePath.toLocal8Bit()));
		return 0;
	}

	QTextCodec *pTextCodec = detectEncoding(originalFile.read(4));
	QTextEncoder *encoder = nullptr;
	QTextDecoder *decoder = nullptr;
	if(pTextCodec != nullptr){
		encoder = pTextCodec->makeEncoder(QTextCodec::ConvertInvalidToNull);
		decoder = pTextCodec->makeDecoder();
	}
	originalFile.seek(0);

	QStandardItemModel *replaceListModel = new QStandardItemModel;
	QStandardItem *replaceListItem = new QStandardItem;
	replaceListItem->setAccessibleText(originalFilePath.toLocal8Bit());
	replaceListItem->setText(tr("Processing ") + originalFilePath);
	replaceListModel->appendRow(replaceListItem);

	Qt::CaseSensitivity cs = Qt::CaseInsensitive;
	if (m_isCase)
	{
		cs = Qt::CaseSensitive;
	}
	QRegExp regExp(m_strSearchPattern, cs);

	int lineNum = 0; // For counting file line.
	int replaceCount = 0;
	qint64 n64SearchLength = 0;
	while(!originalFile.atEnd())
	{
		
		int readSize = 0;
		int indexLineBreak = 0;
		QString line = 0;
		
		QByteArray array = originalFile.readLine();
		if (decoder != nullptr)
		{
			line = decoder->toUnicode(array);
		}
		else
		{
			line.append(QString::fromAscii(array.begin(), array.size()));
		}
		lineNum++;

		bool isReplace = false;
		QString replaceLine;

		int offset = 0;
		int index = 0;
		while((index = regExp.indexIn(line, offset)) != -1)
		{
			int matchLength = regExp.matchedLength();

			QStandardItem *matchItem = new QStandardItem();
			QStandardItem *replaceItem = new QStandardItem();

			QString contentLineDisp;
			QString contentLineOper;

			QString::const_iterator head = line.begin() + index;
			QString::const_iterator tail = head + matchLength;

			contentLineDisp = tripControlCharacterDisp(line.begin(), line.end(), head, tail);
			contentLineOper = tripControlCharacter(line.begin(), line.end(), head, tail);
			contentLineDisp = contentLineDisp.trimmed();
			contentLineOper = contentLineOper.trimmed();


			replaceItem->setText(tr("In line %1  ").arg(lineNum).append(contentLineDisp));
			replaceListModel->appendRow(replaceItem);

			QString originalContent = QString::fromRawData(line.begin() + offset, index + matchLength - offset);
			if (processItemReplace(originalContent,  replaceListModel, replaceLine, contentLineDisp))
			{
				replaceCount++;
				isReplace = true;
			}


			matchItem->setText(tr("In line %1  ").arg(lineNum).append(contentLineDisp));
			matchItem->setAccessibleText(contentLineOper.toLocal8Bit());

			searchResultItem->setAccessibleText(originalFilePath.toLocal8Bit());
			searchResultItem->appendRow(matchItem);

			offset = index + matchLength;

		} //while(std::regex_search(searchStart, lineEnd, result, searchPattern))
		replaceLine.append(QString::fromRawData(line.begin() + offset, line.length() - offset));


		if(!isReplace)
		{
			replaceLine = line;
		}

		QByteArray convert;
		if (encoder != nullptr)
		{
			convert = encoder->fromUnicode(replaceLine);
		} 
		else
		{
			convert = replaceLine.toAscii();
		}
		tempFile.write(convert);
		isReplace = false;

	} // while(getline(istreamFile, line))

	delete encoder;
	delete decoder;
	delete replaceListModel;
	return replaceCount;

}
